package core;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class Bdecoder {

	private final InputStream in;

	private int indicator = 0;

	private String special_map = "info";
	private boolean in_special_map = false;
	private final MessageDigest sha_digest;

	public byte[] get_special_map_digest()
	{
		byte[] result = sha_digest.digest();
		return result;
	}

	public void set_special_map_name(String name)
	{
		special_map = name;
	}


	public Bdecoder(InputStream in)
	{
		this.in = in;
		
		try
		{
			sha_digest = MessageDigest.getInstance("SHA");
		}
		catch(NoSuchAlgorithmException nsa)
		{
			throw new InternalError(nsa.toString());
		}
	}


	public static BeValue bdecode(InputStream in) throws IOException, InvalidBEncodingException
	{
		return new Bdecoder(in).bdecode();
	}


	public int getNextIndicator() throws IOException
	{
		if (indicator == 0)
		{

			indicator = in.read();

			
			if (in_special_map) sha_digest.update((byte)indicator);
		}
		return indicator;
	}

	public BeValue bdecode() throws IOException, InvalidBEncodingException
	{
		indicator = getNextIndicator();		
				
		if (indicator == -1)
			return null;

		if (indicator >= '0' && indicator <= '9')
			return bdecodeBytes();
		else if (indicator == 'i')
			return bdecodeNumber();
		else if (indicator == 'l')
			return bdecodeList();
		else if (indicator == 'd')
			return bdecodeMap();
		else
			throw new InvalidBEncodingException
			("Unknown indicator '" + indicator + "'");
	}


	public BeValue bdecodeBytes() throws IOException, InvalidBEncodingException
	{
		int c = getNextIndicator();
		int num = c - '0';
		if (num < 0 || num > 9)
			throw new InvalidBEncodingException("Number expected, not '"
					+ (char)c + "'");
		indicator = 0;

		c = read();
		int i = c - '0';
		while (i >= 0 && i <= 9)
		{
			
			num = num*10 + i;
			c = read();
			i = c - '0';
		}

		if (c != ':')
			throw new InvalidBEncodingException("Colon expected, not '"
					+ (char)c + "'");

		return new BeValue(read(num));
	}


	public BeValue bdecodeNumber() throws IOException, InvalidBEncodingException
	{
		int c = getNextIndicator();
		if (c != 'i')
			throw new InvalidBEncodingException("Expected 'i', not '"
					+ (char)c + "'");
		indicator = 0;

		c = read();
		if (c == '0')
		{
			c = read();
			if (c == 'e')
				return new BeValue(BigInteger.ZERO);
			else
				throw new InvalidBEncodingException("'e' expected after zero,"
						+ " not '" + (char)c + "'");
		}

		
		char[] chars = new char[256];
		int off = 0;

		if (c == '-')
		{
			c = read();
			if (c == '0')
				throw new InvalidBEncodingException("Negative zero not allowed");
			chars[off] = (char)c;
			off++;
		}

		if (c < '1' || c > '9')
			throw new InvalidBEncodingException("Invalid Integer start '"
					+ (char)c + "'");
		chars[off] = (char)c;
		off++;

		c = read();
		int i = c - '0';
		while(i >= 0 && i <= 9)
		{
			chars[off] = (char)c;
			off++;
			c = read();
			i = c - '0';
		}

		if (c != 'e')
			throw new InvalidBEncodingException("Integer should end with 'e'");

		String s = new String(chars, 0, off);
		return new BeValue(new BigInteger(s));
	}


	public BeValue bdecodeList() throws IOException, InvalidBEncodingException
	{
		int c = getNextIndicator();
		if (c != 'l')
			throw new InvalidBEncodingException("Expected 'l', not '"
					+ (char)c + "'");
		indicator = 0;

		List result = new ArrayList();
		c = getNextIndicator();
		while (c != 'e')
		{
			result.add(bdecode());
			c = getNextIndicator();
		}
		indicator = 0;

		return new BeValue(result);
	}


	public BeValue bdecodeMap() throws IOException, InvalidBEncodingException
	{
		int c = getNextIndicator();
		if (c != 'd')
			throw new InvalidBEncodingException("Expected 'd', not '"
					+ (char)c + "'");
		indicator = 0;

		Map result = new HashMap();
		c = getNextIndicator();
		while (c != 'e')
		{
			
			String key = bdecode().getString();

			
			boolean special = special_map.equals(key);
			if (special)
				in_special_map = true;

			BeValue value = bdecode();
			result.put(key, value);

			
			if (special)
				in_special_map = false;

			c = getNextIndicator();
		}
		indicator = 0;

		return new BeValue(result);
	}

	private int read() throws IOException
	{
		int c = in.read();
		if (c == -1)
			throw new EOFException();
		if (in_special_map) sha_digest.update((byte)c);
		return c;
	}


	private byte[] read(int length) throws IOException
	{
		byte[] result = new byte[length];

		int read = 0;
		while (read < length)
		{
			int i = in.read(result, read, length - read);
			if (i == -1)
				throw new EOFException();
			read += i;
		}

		if (in_special_map) sha_digest.update(result, 0, length);
		return result;
	}

}